Each pizza glides into a slot like a circuit board into a computer, clicks into place as the smart box interfaces with the onboard system of the car. The address of the customer is communicated to the car, which computes and projects the optimal route on a heads-up display.
—Neal Stephenson, Snow Crash
In addition to the processor and memory, most embedded systems contain a handful of other hardware devices. Some of these devices are specific to the application domain, while others—like timers and serial ports—are useful in a wide variety of systems. The most generically useful of these are often included within the same chip as the processor and are called internal, or on-chip, peripherals. Hardware devices that reside outside the processor chip are, therefore, said to be external peripherals. In this chapter we'll discuss the most common software issues that arise when interfacing to a peripheral of either type.
The basic interface between an embedded processor and a peripheral device is a set of control and status registers. These registers are part of the peripheral hardware, and their locations, size, and individual meanings are features of the peripheral. For example, the registers within a serial controller are very different from those in a timer/counter. In this section, I'll describe how to manipulate the contents of these control and status registers directly from your C/C++ programs.
Depending upon the design of the processor and board, peripheral devices are located either in the processor's memory space or within the I/O space. In fact, it is common for embedded systems to include some peripherals of each type. These are called memory-mapped and I/O-mapped peripherals, respectively. Of the two types, memory-mapped peripherals are generally easier to work with and are increasingly popular.
Memory-mapped control and status registers can be made to look just like ordinary variables. To do this, you need simply declare a pointer to the register, or block of registers, and set the value of the pointer explicitly. For example, if the P2LTCH register from Chapter 2, were memory-mapped and located at physical address 7205Eh, we could have implemented toggleLed entirely in C, as shown below. A pointer to an unsigned short—a 16-bit register—is declared and explicitly initialized to the address 0x7200:005E. From that point on, the pointer to the register looks just like a pointer to any other integer variable:
unsigned short * pP2LTCH = (unsigned short *) 0x7200005E; void toggleLed(void) { *pP2LTCH ^= LED_GREEN; /* Read, xor, and modify. */ } /* toggleLed() */
Note, however, that there is one very important difference between device registers and ordinary variables. The contents of a device register can change without the knowledge or intervention of your program. That's because the register contents can also be modified by the peripheral hardware. By contrast, the contents of a variable will not change unless your program modifies them explicitly. For that reason, we say that the contents of a device register are volatile, or subject to change without notice.
The C/C++ keyword volatile should be used when declaring pointers to device registers. This warns the compiler not to make any assumptions about the data stored at that address. For example, if the compiler sees a write to the volatile location followed by another write to that same location, it will not assume that the first write is an unnecessary use of processor time. In other words, the keyword volatile instructs the optimization phase of the compiler to treat that variable as though its behavior cannot be predicted at compile time.
Here's an example of the use of volatile to warn the compiler about the P2LTCH register in the previous code listing:
volatile unsigned short * pP2LTCH = (unsigned short *) 0x7200005E;
It would be wrong to interpret this statement to mean that the pointer itself is volatile. In fact, the value of the variable pP2LTCH will remain 0x7200005E for the duration of the program (unless it is changed somewhere else, of course). Rather, it is the data pointed to that is subject to change without notice. This is a very subtle point, and it is easy to confuse yourself by thinking about it too much. Just remember that the location of a register is fixed, though its contents might not be. And if you use the volatile keyword, the compiler will assume the same.
The primary disadvantage of the other type of device registers, I/O-mapped registers, is that there is no standard way to access them from C or C++. Such registers are accessible only with the help of special machine-language instructions. And these processor-specific instructions are not supported by the C or C++ language standards. So it is necessary to use special library routines or inline assembly (as we did in Chapter 2) to read and write the registers of an I/O-mapped device.
When it comes to designing device drivers, you should always focus on one easily stated goal: hide the hardware completely. When you're finished, you want the device driver module to be the only piece of software in the entire system that reads or writes that particular device's control and status registers directly. In addition, if the device generates any interrupts, the interrupt service routine that responds to them should be an integral part of the device driver. In this section, I'll explain why I recommend this philosophy and how it can be achieved.
Of course, attempts to hide the hardware completely are difficult. Any programming interface you select will reflect the broad features of the device. That's to be expected. The goal should be to create a programming interface that would not need to be changed if the underlying peripheral were replaced with another in its general class. For example, all Flash memory devices share the concepts of sectors (though the sector size can differ between chips). An erase operation can be performed only on an entire sector, and once erased, individual bytes or words can be rewritten. So the programming interface provided by the Flash driver example in the last chapter should work with any Flash memory device. The specific features of the AMD 29F010 are hidden from that level, as desired.
Device drivers for embedded systems are quite different from their workstation counterparts. In a modern computer workstation, device drivers are most often concerned with satisfying the requirements of the operating system. For example, workstation operating systems generally impose strict requirements on the software interface between themselves and a network card. The device driver for a particular network card must conform to this software interface, regardless of the features and capabilities of the underlying hardware. Application programs that want to use the network card are forced to use the networking API provided by the operating system and don't have direct access to the card itself. In this case, the goal of hiding the hardware completely is easily met.
By contrast, the application software in an embedded system can easily access your hardware. In fact, because all of the software is linked together into a single binary image, there is rarely even a distinction made between application software, operating system, and device drivers. The drawing of these lines and the enforcement of hardware access restrictions are purely the responsibilities of the software developers. Both are design decisions that the developers must consciously make. In other words, the implementers of embedded software can more easily cheat on the software design than their non-embedded peers.
The benefits of good device driver design are threefold. First, because of the modularization, the structure of the overall software is easier to understand. Second, because there is only one module that ever interacts directly with the peripheral's registers, the state of the hardware can be more accurately tracked. And, last but not least, software changes that result from hardware changes are localized to the device driver. Each of these benefits can and will help to reduce the total number of bugs in your embedded software. But you have to be willing to put in a bit of extra effort at design time in order to realize such savings.
If you agree with the philosophy of hiding all hardware specifics and interactions within the device driver, it will usually consist of the five components in the following list. To make driver implementation as simple and incremental as possible, these elements should be developed in the order in which they are presented.
The first step in the driver development process is to create a C-style struct that looks just like the memory-mapped registers of your device. This usually involves studying the data book for the peripheral and creating a table of the control and status registers and their offsets. Then, beginning with the register at the lowest offset, start filling out the struct. (If one or more locations are unused or reserved, be sure to place dummy variables there to fill in the additional space.)
An example of such a data structure is shown below. This structure describes the registers in one of the on-chip timer/counter units within the 80188EB processor. The device has three registers, arranged as shown in the TimerCounter data structure below. Each register is 16 bits wide and should be treated as an unsigned integer, although one of them, the control register, is actually a collection of individually significant bits.
struct TimerCounter { unsigned short count; // Current Count, offset 0x00 unsigned short maxCountA; // Maximum Count, offset 0x02 unsigned short _reserved; // Unused Space, offset 0x04 unsigned short control; // Control Bits, offset 0x06 };
To make the bits within the control register easier to read and write individually, we might also define the following bitmasks:
#define TIMER_ENABLE 0xC000 // Enable the timer. #define TIMER_DISABLE 0x4000 // Disable the timer. #define TIMER_INTERRUPT 0x2000 // Enable timer interrupts. #define TIMER_MAXCOUNT 0x0020 // Timer complete? #define TIMER_PERIODIC 0x0001 // Periodic timer?
The second step in the driver development process is to figure out what variables you will need to track the state of the hardware and device driver. For example, in the case of the timer/counter unit described earlier we'll probably need to know if the hardware has been initialized. And if it has been, we might also want to know the length of the running countdown.
Some device drivers create more than one software device. This is a purely logical device that is implemented over the top of the basic peripheral hardware. For example, it is easy to imagine that more than one software timer could be created from a single timer/counter unit. The timer/counter unit would be configured to generate a periodic clock tick, and the device driver would then manage a set of software timers of various lengths by maintaining state information for each.
Once you know how you'll track the state of the physical and logical devices, it's time to start writing the functions that actually interact with and control the device. It is probably best to begin with the hardware initialization routine. You'll need that one first anyway, and it's a good way to get familiar with the device interaction.
After you've successfully initialized the device, you can start adding other functionality to the driver. Hopefully, you've already settled on the names and purposes of the various routines, as well as their respective parameters and return values. All that's left to do now is implement and test each one. We'll see examples of such routines in the next section.
It's best to design, implement, and test most of the device driver routines before enabling interrupts for the first time. Locating the source of interrupt-related problems can be quite challenging. And, if you add possible bugs in the other driver modules to the mix, it could even approach impossible. It's far better to use polling to get the guts of the driver working. That way you'll know how the device works (and that it is indeed working) when you start looking for the source of your interrupt problems. And there will almost certainly be some of those.
The device driver example that we're about to discuss is designed to control one of the timer/counter units contained within the 80188EB processor. I have chosen to implement this driver—and all of the remaining examples in the book—in C++. Although C++ offers no additional assistance over C in accessing hardware registers, there are many good reasons to use it for this type of abstraction. Most notably, C++ classes allow us to hide the actual hardware interface more completely than any C features or programming techniques. For example, a constructor can be included to automatically configure the hardware each time a new timer object is declared. This eliminates the need for an explicit call from the application software to the driver initialization routine. In addition, it is possible to hide the data structure that corresponds to the device registers within the private part of the associated class. This helps to prevent the application programmer from accidentally reading or writing the device registers from some other part of the program.
The definition of the Timer class is as follows:
enum TimerState { Idle, Active, Done }; enum TimerType { OneShot, Periodic }; class Timer { public: Timer(); ~Timer(); int start(unsigned int nMilliseconds, TimerType = OneShot); int waitfor(); void cancel(); TimerState state; TimerType type; unsigned int length; unsigned int count; Timer * pNext; private: static void interrupt Interrupt(); };
Before discussing the implementation of this class, let's examine the previous declaration and consider the device driver's overall structure. The first thing we see are two enumerated types, TimerState and TimerType. The main purpose of these types is to make the rest of the code more readable. From them we learn that each software timer has a current state—Idle, Active, or Done—and a type—OneShot or Periodic. The timer's type tells the driver what to do with the timer when it expires; a Periodic timer is to be restarted then.
The constructor for the Timer class is also the device driver's initialization routine. It ensures that the timer/counter hardware is actively generating a clock tick every 1 millisecond. The other public methods of the class—start, waitfor, and cancel —provide an API for an easy-to-use software timer. These methods allow application programmers to start one-shot and periodic timers, wait for them to expire, and cancel running timers, respectively. This is a much simpler and more generic interface than that provided by the timer/counter hardware within the 80188EB chip. For one thing, the timer hardware does not know about human units of time, like milliseconds. But because the timer driver hides the specifics of this particular hardware, the application programmer need never even know about that.
The data members of the class should also help give you some insight into the device driver implementation. The first three items are variables that answer the following questions about this software timer:
What is the timer's current state (idle, active, or done)?
What type of a timer is it (one-shot or periodic)?
What is the total length of the timer (in units called ticks)?
Following those are two more data members, both of which contain information that is specific to this implementation of the timer driver. The values of count and pNext have meaning only within the context of a linked list of active software timers. This linked list is ordered by the number of ticks remaining for each timer. So count contains information about the number of ticks remaining before this software timer is set to expire,[1] and pNext is a pointer to the software timer that will expire the soonest after this one.
Finally, there is a private method called Interrupt —our interrupt service routine. The Interrupt method is declared static because it is not allowed to manipulate the data members of the individual software timers. So, for example, the interrupt service routine is not allowed to modify the state of any timer. By using the keyword static, this restriction is automatically enforced for us by the C++ compiler.
The most important thing to learn from the class declaration is that, although all of the software timers are driven by the same hardware timer/counter unit, each has its own private data store. This allows the application programmer to create multiple simultaneous software timers and the device driver to manage them behind the scenes. Once you grasp that idea, you're ready to look at the implementation of the driver's initialization routine, API, and interrupt service routine.
The constructor for the Timer class is responsible for initializing both the software timer and the underlying hardware. With respect to the latter, it is responsible for configuring the timer/counter unit, inserting the address of the interrupt service routine into the interrupt vector table, and enabling timer interrupts. However, because this method is a constructor that may be called several times (once for each of the Timer objects declared), our implementation of the constructor must be smart enough to perform these hardware initializations only during the very first call to it. Otherwise, the timer/counter unit might be reset at an inopportune time or become out of sync with the device driver.
That is the reason for the static variable bInitialized in the following code. This variable is declared with an initial value of zero and set to one after the hardware initialization sequence has been performed. Subsequent calls to the Timer constructor will see that bInitialized is no longer zero and skip that part of the initialization sequence.
#include "i8018xEB.h" #include "timer.h" #define CYCLES_PER_TICK (25000/4) // Number of clock cycles per tick. /********************************************************************** * * Method: Timer() * * Description: Constructor for the Timer class. * * Notes: * * Returns: None defined. * **********************************************************************/ Timer::Timer(void) { static int bInitialized = 0; // // Initialize the new software timer. // state = Idle; type = OneShot; length = 0; count = 0; pNext = NULL; // // Initialize the timer hardware, if not previously done. // if (!bInitialized) { // // Install the interrupt handler and enable timer interrupts. // gProcessor.installHandler(TIMER2_INT, Timer::Interrupt); gProcessor.pPCB->intControl.timerControl &= ~(TIMER_MASK | TIMER_PRIORITY); // // Initialize the hardware device (use Timer #2). // gProcessor.pPCB->timer[2].count = 0; gProcessor.pPCB->timer[2].maxCountA = CYCLES_PER_TICK; gProcessor.pPCB->timer[2].control = TIMER_ENABLE | TIMER_INTERRUPT | TIMER_PERIODIC; // // Mark the timer hardware initialized. // bInitialized = 1; } } /* Timer() */
The global object gProcessor is declared in a header file called i8018xEB.h. It represents the Intel 80188EB processor. The i8018xEB class is something that I wrote, and it includes methods to make interaction with the processor and its on-chip peripherals easier. One of these methods is called installHandler, and its job is to insert an interrupt service routine into the interrupt vector table. This class also includes a global data structure called PCB that can be overlaid upon the memory-mapped registers of the peripheral control block.[2] The three registers associated with timer/counter unit 2 make up just one small part of this 256-byte structure. (For purely aesthetic reasons, I've implemented the PCB data structure as a set of nested structures. Hence, the control register of timer/counter unit 2 is accessible as pPCB->timer[2].control.)
The initialization of the timer/counter unit consists of resetting its count register to 0, loading the maxCountA register with the countdown length, and setting several bits within the control register. What we are doing above is starting a 1 ms periodic timer that generates an interrupt at the end of each cycle. (This periodic timer will act as the clock tick we need to create software timers of arbitrary lengths.) The value that is loaded into maxCountA can be determined mathematically because it represents the number of clock cycles input to the timer/counter unit in a 1 ms period. According to the 80188EB databook, this will be one fourth of the number of processor cycles in a 1 ms period. So, for a 25 MHz processor like the one we're using (that's 25,000,000 cycles per second, or, if you prefer, 25,000 cycles per millisecond), maxCountA should be set to 25,000/4—as it is in the constant CYCLES_PER_TICK earlier.
Once the hardware has been initialized and the clock tick established, it is possible to start a software timer of any length, so long as that length can be expressed as an integral number of ticks. Because our clock tick is 1 ms long, the application programmer can create timers of any length from 1 to 65,535 ms (65.536 seconds). He would do this by calling the start method:
/********************************************************************** * * Method: start() * * Description: Start a software timer, based on the tick from the * underlying hardware timer. * * Notes: * * Returns: 0 on success, -1 if the timer is already in use. * **********************************************************************/ int Timer::start(unsigned int nMilliseconds, TimerType timerType) { if (state != Idle) { return (-1); } // // Initialize the software timer. // state = Active; type = timerType; length = nMilliseconds / MS_PER_TICK; // // Add this timer to the active timer list. // timerList.insert(this); return (0); } /* start() */
When a software timer is started, the data members state, type, and length are initialized and the timer is inserted into a linked list of active timers called the timerList. The timers in the timer list are ordered so that the first timer to expire is at the top of the list. In addition, each timer has a count associated with it. This value represents the number of ticks that will be remaining in the software timer once all previous timers in the list have expired. Taken together, these design choices favor quick updates to the timer list at the price of slower insertions and deletions. Speed is important during updates because the timer list will be updated every time the hardware generates a clock tick interrupt—that's every one millisecond.
Figure 7-1 shows the timer list in action. Remember that each software timer has its own unique length and starting time, but once it has been inserted into the list, only the count field matters for ordering. In the example shown, the first and second timers were both started (the second might actually have been restarted, because it is periodic) at the same time. Since the second is 5 ms longer, it will expire 5 clock ticks after the first. The second and third timers in the list both happen to expire at the same time, though the third timer will have been running for 10 times longer.
The code for the interrupt service routine is shown below. This routine is declared to be of type void interrupt. The keyword interrupt is an extension of the C/C++ language that is understood only by compilers for 80x86 processors. By declaring the routine in this way, we ask the compiler to save and restore all of the processor's registers at the entry and exit, rather than only those that are saved during an ordinary function call.
/********************************************************************** * * Method: Interrupt() * * Description: An interrupt handler for the timer hardware. * * Notes: This method is declared static, so that we cannot * inadvertently modify any of the software timers. * * Returns: None defined. * **********************************************************************/ void interrupt Timer::Interrupt() { // // Decrement the active timer's count. // timerList.tick(); // // Acknowledge the timer interrupt. // gProcessor.pPCB->intControl.eoi = EOI_NONSPECIFIC; // // Clear the Maximum Count bit (to start the next cycle). // gProcessor.pPCB->timer[2].control &= ~TIMER_MAXCOUNT; } /* Interrupt() */
Of course, the tick method of the TimerList class does most of the work here. This method is mostly concerned with linked list manipulation and is not very exciting to look at. Briefly stated, the tick method starts by decrementing the tick count of the timer at the top of the list. If that timer's count has reached zero, it changes the state of the software timer to Done and removes it from the timer list. It also does the same for any timers that are set to expire on the very same tick. These are the ones at the new head of the list that also have a count of zero.
After creating and starting a software timer, the application programmer can do some other processing and then check to see if the timer has expired. The waitfor method is provided for that purpose. This routine will block until the software timer's state is changed to Done by timerList.tick. The implementation of this method is as follows:
/********************************************************************** * * Method: waitfor() * * Description: Wait for the software timer to finish. * * Notes: * * Returns: 0 on success, -1 if the timer is not running. * **********************************************************************/ int Timer::waitfor() { if (state != Active) { return (-1); } // // Wait for the timer to expire. // while (state != Done); // // Restart or idle the timer, depending on its type. // if (type == Periodic) { state = Active; timerList.insert(this); } else { state = Idle; } return (0); } /* waitfor() */
One important thing to notice about this code is that the test while (state != Done) is not an infinite loop. That's because, as we just learned a few paragraphs back, the timer's state is modified by timerList.tick, which is called from the interrupt service routine. In fact, if we were being careful embedded programmers, we would have declared state as volatile. Doing so would prevent the compiler from incorrectly assuming that the timer's state is either done or not done and optimizing away the while loop.[3]
The final method of the Timer class is used to cancel a running timer. This is easy to implement because we need only remove the timer from the timer list and change its state to Idle. The code that actually does this is shown here:
/********************************************************************** * * Method: cancel() * * Description: Stop a running timer. * * Notes: * * Returns: None defined. * **********************************************************************/ void Timer::cancel(void) { // // Remove the timer from the timer list. // if (state == Active) { timerList.remove(this); } // // Reset the timer's state. // state = Idle; } /* cancel() */
Of course, there is also a destructor for the Timer class, though I won't show the code here. Suffice it to say that it just checks to see if the software timer is active and, if so, removes it from the timer list. This prevents a periodic timer that has gone out of scope from remaining in the timer list indefinitely and any pointers to the "dead" timer from remaining in the system.
For completeness, it might be nice to add a public method, perhaps called poll, that allows users of the Timer class to test the state of a software timer without blocking. In the interest of space, I have left this out of my implementation, but it would be easy to add such a routine. It need only return the current value of the comparison state == Done. However, in order to do this, some technique would need to be devised to restart periodic timers for which waitfor is never called.
Watchdog TimersAnother type of timer you might hear mentioned frequently in reference to embedded systems is a watchdog timer. This is a special piece of hardware that protects the system from software hangs. If present, the watchdog timer is always counting down from some large number to zero. This process typically takes a few seconds to complete. In the meantime, it is possible for the embedded software to "kick" the watchdog timer, to reset its counter to the original large number. If the counter ever does reach zero, the watchdog timer will assume that the software is hung. It then resets the embedded processor and, thus, restarts the software. This is a common way to recover from unexpected software hangs that occur after the system is deployed. For example, suppose that your company's new product will travel into space. No matter how much testing you do before deployment, the possibility remains that there are undiscovered bugs lurking in the software and that one or more of these is capable of hanging the system altogether. If the software hangs, you won't be able to communicate with it at all, so you can't just issue a reset command remotely. Instead, you must build an automatic recovery mechanism into the system. And that's where the watchdog timer comes in. The implementation of the watchdog timer "kick" would look just like the Blinking LED program in this chapter, except that instead of toggling the LED the watchdog timer's counter would be reset. |
Another potential feature of the Timer class is asynchronous callbacks. In other words, why not allow the creator of a software timer to attach a function to it. This function could then be called automatically—via timerList.tick —each time that timer expires. As you read the next section, be sure to think about how different the Blinking LED program would look if asynchronous callbacks were used instead. This is one type of application to which asynchronous function calls are particularly well suited.
Now that we have the Timer class at our disposal, it is possible to rewrite the book's very first example to make its timing more precise. Recall that in our original implementation, we relied on the fact that the length of a "decrement and compare" operation was fixed for a given processor and speed. We simply took a guess as to how long that might be and then revised our estimate based on empirical testing. By utilizing the Timer class, we can simultaneously eliminate this guesswork and increase the readability of the program.
In the revised Blinking LED program below you will see that we can now simply start a periodic 500 ms software timer, toggle the LED, and then wait for the timer to expire before toggling the LED again. In the meantime, we could perform other processing tasks required by the application at hand.
#include "timer.h" #include "led.h" /********************************************************************** * * Function: main() * * Description: Blink the green LED once a second. * * Notes: This outer loop is hardware-independent. However, it * calls the hardware-dependent function toggleLed(). * * Returns: This routine contains an infinite loop. * **********************************************************************/ void main(void) { Timer timer; timer.start(500, Periodic); // Start a periodic 500 ms timer. while (1) { toggleLed(LED_GREEN); // Toggle the green LED. // Do other useful work here. timer.waitfor(); // Wait for the timer to expire. } } /* main() */
[1] Specifically, it represents the number of clock ticks remaining after all of the timers ahead of it in the list have expired.
[2] Astute readers might recall that in Chapter 5, I stated that the PCB was located in the I/O space of the 80188EB processor. However, because memory-mapped registers are more likely in a device driver situation, I've relocated the entire PCB to physical address 72000h, in the memory space. This new location will be assumed for the rest of the book. To see how this relocation was performed, take a look at the constructor for the i8018xEB class.
[3] A word of caution about waitfor : this implementation spins its wheels waiting for the software timer to change to the done state. This technique is called busy-waiting, and it is neither elegant nor an efficient use of the processor. In Chapter 8, we'll see how the introduction of an operating system allows us to improve upon this implementation.